/**
 * @author Toru Nagashima <https://github.com/mysticatea>
 * @copyright 2017 Toru Nagashima. All rights reserved.
 * See LICENSE file in root directory for full license.
 */
import type { VisitorKeys } from "eslint-visitor-keys"
import * as Evk from "eslint-visitor-keys"
import type { Node } from "./nodes"

//------------------------------------------------------------------------------
// Helpers
//------------------------------------------------------------------------------

export const KEYS = Evk.unionWith({
    VAttribute: ["key", "value"],
    VDirectiveKey: ["name", "argument", "modifiers"],
    VDocumentFragment: ["children"],
    VElement: ["startTag", "children", "endTag"],
    VEndTag: [],
    VExpressionContainer: ["expression"],
    VFilter: ["callee", "arguments"],
    VFilterSequenceExpression: ["expression", "filters"],
    VForExpression: ["left", "right"],
    VIdentifier: [],
    VLiteral: [],
    VOnExpression: ["body"],
    VSlotScopeExpression: ["params"],
    VStartTag: ["attributes"],
    VText: [],
    VGenericExpression: ["params"],
})

/**
 * Check that the given key should be traversed or not.
 * @param key The key to check.
 * @param value The value of the key in the node.
 * @returns `true` if the key should be traversed.
 */
function fallbackKeysFilter(key: string, value: any = null): boolean {
    return (
        key !== "comments" &&
        key !== "leadingComments" &&
        key !== "loc" &&
        key !== "parent" &&
        key !== "range" &&
        key !== "tokens" &&
        key !== "trailingComments" &&
        value !== null &&
        typeof value === "object" &&
        (typeof value.type === "string" || Array.isArray(value))
    )
}

/**
 * Get the keys of the given node to traverse it.
 * @param node The node to get.
 * @returns The keys to traverse.
 */
export function getFallbackKeys(node: Node): string[] {
    return Object.keys(node).filter((key) =>
        fallbackKeysFilter(key, node[key as keyof Node]),
    )
}

/**
 * Check wheather a given value is a node.
 * @param x The value to check.
 * @returns `true` if the value is a node.
 */
function isNode(x: any): x is Node {
    return x !== null && typeof x === "object" && typeof x.type === "string"
}

/**
 * Traverse the given node.
 * @param node The node to traverse.
 * @param parent The parent node.
 * @param visitor The node visitor.
 */
function traverse(node: Node, parent: Node | null, visitor: Visitor): void {
    let i = 0
    let j = 0

    visitor.enterNode(node, parent)

    const keys =
        (visitor.visitorKeys ?? KEYS)[node.type] ?? getFallbackKeys(node)
    for (i = 0; i < keys.length; ++i) {
        const child = (node as any)[keys[i]]

        if (Array.isArray(child)) {
            for (j = 0; j < child.length; ++j) {
                if (isNode(child[j])) {
                    traverse(child[j], node, visitor)
                }
            }
        } else if (isNode(child)) {
            traverse(child, node, visitor)
        }
    }

    visitor.leaveNode(node, parent)
}

//------------------------------------------------------------------------------
// Exports
//------------------------------------------------------------------------------

export interface Visitor {
    visitorKeys?: VisitorKeys
    enterNode(node: Node, parent: Node | null): void
    leaveNode(node: Node, parent: Node | null): void
}

/**
 * Traverse the given AST tree.
 * @param node Root node to traverse.
 * @param visitor Visitor.
 */
export function traverseNodes(node: Node, visitor: Visitor): void {
    traverse(node, null, visitor)
}
